/*
 * Copyright 2008 Max Kugland
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.splink.library.queue {
	import org.splink.library.utils.ArrayUtils;

	/**
	 * The default <code>IQ</code> implementation
	 * 
	 * @see org.splink.library.queue.IQ
	 * 
	 * @author Max Kugland
	 */
	public class Q extends Qable implements IQ {
		private var _items : Array = new Array();		private var _currentItem : IQableDecorator;
		private var _index : uint = 0;		private var _isRunning : Boolean = false;
		private var _current : uint = 1;
		private var _lastProgressEvent : QEvent;

		public function Q(id : String = "Q") {
			super(id);
		}

		/**
		 * @inheritDoc
		 */
		public function add(qable : IQable) : IQable {
			if(qable is IQ) {
				qable = new QDecorator(IQ(qable));
			} else {
				qable = new QableDecorator(qable);
			}
			_items.push(qable);
			return QableDecorator(qable).getIQable();
		}

		/**
		 * @inheritDoc
		 */
		public function remove(qable : IQable) : IQable {
			for each (var item : IQableDecorator in _items) {
				if(item.getIQable() == qable) {
					ArrayUtils.remove(_items, item);
					return qable;
				}
			}
						return qable;
		}

		/**
		 * In contrast to this instance method <code>getForId</code> which only considers
		 * the <code>Qable</code> items which have been diurectly added to the <code>IQ</code>
		 * there is a static getForId method <code>Q.getForId</code> which performs a recursive 
		 * search throughout nested <code>IQ</code>s for the item with the given id.
		 * 
		 * @inheritDoc
		 */
		public function getForId(id : String) : IQable {
			for each (var item : IQableDecorator in _items) {
				if(item.id == id) {
					return item.getIQable();
				} 
			}
			
			return null;
		}

		/**
		 * @inheritDoc
		 */
		public function get current() : IQable {
			return _currentItem ? _currentItem.getIQable() : null;
		}

		/**
		 * @inheritDoc
		 */
		override protected function doReset() : void {
			_index = 0;
			if(_currentItem) {
				unregisterItem(_currentItem);
			}

			for each (var item : IQable in _items) {
				item.reset();
			}
			_current = 1;
			_isRunning = false;
		}

		/**
		 * @inheritDoc
		 */
		override protected function doStart() : void {
			if(_index < _items.length) {
				_currentItem = _items[_index];
				registerItem(_currentItem);
				
				_isRunning = true;
				_currentItem.start();
			} else {
				_isRunning = false;
				complete();
				_currentItem = null;
			}
		}

		/**
		 * @inheritDoc
		 */
		override protected function doStop() : void {
			if(_currentItem == null || !_isRunning) {
				return;
			}

			_isRunning = false;
			_currentItem.stop();
			unregisterItem(_currentItem);
			if(_currentItem.isComplete()) {
				_index++;
				distributeProgress();
			}
		}

		private function onItemError(e : QEvent) : void {
			error(e.errorMessage);
			
			if(!(e.source is IQ)) {
				onItemComplete();
			}
		}

		private function onItemComplete(e : QEvent = null) : void {
			distributeProgress();
			unregisterItem(_currentItem);
			_index++;
			
			/**
			 * We use <code>doStart</code> instead of <code>start</code> 
			 * because the <code>QEvent.START</code> should only fire once 
			 * and not for each enqueued item.
			 */
			doStart(); 
		}

		private function registerItem(item : IQable) : void {
			if(item) {
				item.register(QEvent.COMPLETE, onItemComplete);
				item.register(QEvent.ERROR, onItemError);
				item.register(QEvent.PROGRESS, distributeProgress);
			}
		}

		private function unregisterItem(item : IQable) : void {
			if(item) {
				item.unregister(QEvent.COMPLETE, onItemComplete);
				item.unregister(QEvent.ERROR, onItemError);				item.unregister(QEvent.PROGRESS, distributeProgress);
			}
		}

		private function distributeProgress(progressEvent : QEvent = null) : void {
			if(progressEvent == null) {
				progressEvent = new QEvent(this, QEvent.PROGRESS, id);
			}
			
			progressEvent.queueProgress.current = _current;			progressEvent.queueProgress.total = getTotal(this, 0);
			
			if(progressEvent.source is IQ) {
				_current++;
			} 
			
			// events fired by an IQ don't contain information on the item progress
			// in case there is item progress information present it is copied onto the event
			if(progressEvent.progress.total == 0 && _lastProgressEvent && _lastProgressEvent.queueProgress.toString() == progressEvent.queueProgress.toString()) {				progressEvent.progress.current = _lastProgressEvent.progress.current;				progressEvent.progress.total = _lastProgressEvent.progress.total;
			} 
			
			distribute(_lastProgressEvent = progressEvent);
		}

		/**
		 * @inheritDoc
		 */
		public function get length() : int {
			return _items.length;
		}

		/**
		 * @inheritDoc
		 */
		public function getAt(index : uint) : IQable {
			return IQableDecorator(_items[index]).getIQable();
		}

		/**
		 * @inheritDoc
		 */
		public function get isRunning() : Boolean {
			return _isRunning;
		}

		/**
		 * @inheritDoc
		 */
		override protected function doFinalize() : void {
			_isRunning = false;
			for each (var item : IQable in _items) {
				item.finalize();
			}
			_currentItem = null;
			_items = new Array();			_current = 1;
			_index = 0;		}

		/**
		 * @param q the <code>IQ</code> from which to start counting
		 * @return the total number of enqueued items within the given <code>IQ</code>
		 * and it's children
		 */
		public static function getTotal(q : IQ, count : int = 0) : int {
			for (var i : int = 0;i < q.length;i++) {
				var qable : IQable = q.getAt(i);
				if(qable is IQ) {
					count += getTotal(IQ(qable));
				} 
				count++;
			}
			return count;
		}

		/**
		 * Visits all <code>IQable</code> beneath the given <code>IQ</code> and invokes the given 
		 * function with each of the  <code>IQable</code> as argument.
		 * 
		 * @param q the <code>IQ</code> whose children (and their children,...) are visited
		 * @param fnc a function with one parameter typed as <code>IQable</code> 
		 */
		public static function visit(q : IQ, fnc : Function) : void {
			for (var i : int = 0;i < q.length;i++) {
				var qable : IQable = q.getAt(i);
				fnc(qable);
				if(qable is IQ) {
					visit(IQ(qable), fnc);
				}
			}
		}

		/**
		 * Searches the given <code>IQ</code> and all descendents for an item
		 * with the given id and returns the item. If no matching item can be
		 * found null is returned
		 * 
		 * @param q the <code>IQ</code> to search
		 * @param id the id of the item to find
		 * @return the <code>IQable</code> or null
		 */
		public static function getForId(q : IQ, id : String) : IQable {
			for (var i : int = 0;i < q.length;i++) {
				var item : IQable = q.getAt(i);
				if(item.id == id) {
					return item;
				} else if(item is IQ) {
					var r : IQable = Q.getForId(IQ(item), id);
					if(r) {
						return r;
					}
				}
			}
			
			return null;
		}
		
		/**
		 * Searches the given <code>IQ</code> and all descendents for the given qable
		 * and removes the qable from the <code>IQ</code> it is currently part of
		 * 
		 * @param q the <code>IQ</code> to search
		 * @param qable the item to remove
		 * @return the <code>IQable</code> or null
		 */
		public static function remove(q : IQ, qable : IQable) : IQable {
			for (var i : int = 0;i < q.length;i++) {
				var item : IQable = q.getAt(i);
				if(item == qable) {
					return q.remove(qable);
				} else if(item is IQ) {
					var r : IQable  = Q.remove(IQ(item), qable);
					if(r) return r;
				}
			}
			return null;
		}

		/**
		 * Recursively searches for the currently processing <code>IQable</code>.
		 * In contrast to the instance method <code>current</code> the actual
		 * <code>IQable</code> is returned and not the (first) enclosing <code>IQ</code>
		 * 
		 * @param q the <code>IQ</code> to start looking
		 * @return the <code>IQable</code> or null
		 */
		public static function getCurrent(q : IQ) : IQable {
			if(q.current) {
				if(q.current is IQ) {
					return Q.getCurrent(IQ(q.current));
				} else {
					return q.current;		
				}
			}
			
			return null;
		}
	}
}
